﻿using Common.FrontEnd;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using Util;
using Util.Props;

namespace FrontEnd.Util
{
    /// <summary>
    /// A StreamDataSource converts data from an InputStream into Data objects. One
    /// would call {@link #setInputStream(InputStream,String) setInputStream} to set
    /// the input stream, and call {@link #getData} to obtain the Data object. The
    /// InputStream can be an arbitrary stream, for example a data from the network
    /// or from a pipe.
    ///
    /// StreamDataSource is not aware about incoming data format and assumes
    /// that incoming data matches StreamDataSource configuration. By default it's configured
    /// to read 16 kHz little-endian 16-bit signed raw data. If data has wrong format
    /// the result of the recognition is undefined. Also note that the sample rate of the
    /// data must match the sample required by the the acoustic model. If your
    /// model decodes 16 kHz files you can't recognize 8kHz data using it.
    ///
    /// You can use AudioFileDataSource instead to read the file headers and
    /// to convert incoming data to the required format automatically.
    /// </summary>
    public class StreamDataSource : BaseDataProcessor
    {
        /// <summary>
        /// The property for the sample rate. 
        /// </summary>
        [S4Integer(defaultValue = 16000)]
        public static String PROP_SAMPLE_RATE = "sampleRate";

        /// <summary>
        /// The property for the number of bytes to read from the InputStream each
        /// time.
        /// </summary>
        [S4Integer(defaultValue = 3200)]
        public static String PROP_BYTES_PER_READ = "bytesPerRead";

        /// <summary>
        /// The property for the number of bits per value. 
        /// </summary>
        [S4Integer(defaultValue = 16)]
        public static String PROP_BITS_PER_SAMPLE = "bitsPerSample";

        /// <summary>
        /// The property specifying whether the input data is big-endian. 
        /// </summary>
        [S4Boolean(defaultValue = false)]
        public static String PROP_BIG_ENDIAN_DATA = "bigEndianData";

        /// <summary>
        /// The property specifying whether the input data is signed. 
        /// </summary>
        [S4Boolean(defaultValue = true)]
        public static String PROP_SIGNED_DATA = "signedData";

        private FileStream dataStream;
        protected int sampleRate;
        private int bytesPerRead;
        private int bytesPerValue;
        private long totalValuesRead;
        private Boolean bigEndian;
        private Boolean signedData;
        private Boolean streamEndReached;
        private Boolean utteranceEndSent;
        private Boolean utteranceStarted;
        protected int bitsPerSample;

        private TimeFrame timeFrame = TimeFrame.INFINITE;

        public StreamDataSource(int sampleRate, int bytesPerRead,
                int bitsPerSample, Boolean bigEndian, Boolean signedData) 
        {
            init(sampleRate, bytesPerRead, bitsPerSample, bigEndian, signedData);
        }

        public StreamDataSource() {

        }

        /// <summary>
        /// @see Sphincs.util.props.Configurable#newProperties(Sphincs.util.props.PropertySheet)
        /// </summary>
        /// <param name="ps"></param>
        override public void newProperties(PropertySheet ps)
        {
            base.newProperties(ps);
            init(
                 ps.getInt(PROP_SAMPLE_RATE),
                 ps.getInt(PROP_BYTES_PER_READ),
                 ps.getInt(PROP_BITS_PER_SAMPLE),
                 ps.getBoolean(PROP_BIG_ENDIAN_DATA),
                 ps.getBoolean(PROP_SIGNED_DATA));
        }

        private void init(int sampleRate,
                          int bytesPerRead,
                          int bitsPerSample,
                          Boolean bigEndian,
                          Boolean signedData) 
        {
            this.sampleRate = sampleRate;
            this.bytesPerRead = bytesPerRead;
            this.bitsPerSample = bitsPerSample;

            if (this.bitsPerSample % 8 != 0)
                throw new ArgumentOutOfRangeException("bits per sample must be a multiple of 8");

            this.bytesPerValue = bitsPerSample / 8;
            this.bigEndian = bigEndian;
            this.signedData = signedData;
            this.bytesPerRead += bytesPerRead % 2;
        }

        /// <summary>
        /// Sphincs.FrontEnd.DataProcessor#initialize(sphincs.FrontEnd.CommonConfig)
        /// </summary>
        override public void initialize() 
        {
            base.initialize();
        }

        public void setInputStream(FileStream inputStream) 
        {
            setInputStream(inputStream, TimeFrame.INFINITE);
        }

        /**
        /// Sets the InputStream from which this StreamDataSource reads.
         *
        /// @param inputStream the InputStream from which audio data comes
         */
        public void setInputStream(FileStream inputStream, TimeFrame timeFrame) 
        {
            dataStream = inputStream;
            this.timeFrame = timeFrame;
            streamEndReached = false;
            utteranceEndSent = false;
            utteranceStarted = false;
            totalValuesRead = 0;
        }

        /// <summary>
        /// Reads and returns the next Data from the InputStream of
        /// StreamDataSource, return null if no data is read and end of file is
        /// reached.
        /// </summary>
        /// <returns>the next Data or <code>null</code> if none is available</returns>
        override public IData getData()
        {
            getTimer().start();
            IData output = null;
            if (streamEndReached) 
            {
                if (!utteranceEndSent) 
                {
                    // since 'firstSampleNumber' starts at 0, the last
                    // sample number should be 'totalValuesRead - 1'
                    output = new DataEndSignal(getDuration());
                    utteranceEndSent = true;
                }
            } 
            else 
            {
                if (!utteranceStarted) 
                {
                    utteranceStarted = true;
                    output = new DataStartSignal(sampleRate);
                } 
                else 
                {
                    if (dataStream != null) 
                    {
                        do 
                        {
                            output = readNextFrame();
                        } 
                        while (output != null && getDuration() < timeFrame.getStart());

                        if ((output == null || getDuration() > timeFrame.getEnd())
                                && !utteranceEndSent) 
                        {
                            output = new DataEndSignal(getDuration());
                            utteranceEndSent = true;
                            streamEndReached = true;
                        }
                    } 
                    else 
                    {
                        Trace.WriteLine("Input stream is not set");
                        if (!utteranceEndSent) 
                        {
                            output = new DataEndSignal(getDuration());
                            utteranceEndSent = true;
                        }
                    }
                }
            }
            getTimer().stop();
            return output;
        }

        /// <summary>
        /// Returns the next Data from the input stream, or null if there is none
        /// available
        /// </summary>
        /// <returns>a Data or null</returns>
        private DoubleData readNextFrame()
        {
            // read one frame's worth of bytes
            int read=0;
            int totalRead = 0;
            int bytesToRead = bytesPerRead;
            byte[] samplesBuffer = new byte[bytesPerRead];
            long firstSample = totalValuesRead;
            try {
                do 
                {
                    read = dataStream.Read(samplesBuffer, totalRead, bytesToRead - totalRead);
                    if (read > 0) 
                    {
                        totalRead += read;
                    }
                } 
                while (read != 0 && totalRead < bytesToRead);
                
                if (totalRead <= 0) 
                {
                    closeDataStream();
                    return null;
                }
                // shrink incomplete frames
                totalValuesRead += (totalRead / bytesPerValue);
                if (totalRead < bytesToRead) 
                {
                    totalRead = (totalRead % 2 == 0)
                            ? totalRead + 2
                            : totalRead + 3;
                    byte[] shrinkedBuffer = new byte[totalRead];
                    Array.Copy(samplesBuffer, 0, shrinkedBuffer, 0, totalRead);
                    samplesBuffer = shrinkedBuffer;
                    closeDataStream();
                }
            } 
            catch (IOException ioe) 
            {
                throw new Exception("Error reading data", ioe);
            }
            // turn it into an Data object
            double[] doubleData;
            if (bigEndian) 
            {
                doubleData = DataUtil.bytesToValues(samplesBuffer, 0, totalRead,
                                                    bytesPerValue, signedData);
            } 
            else 
            {
                doubleData = DataUtil.littleEndianBytesToValues(samplesBuffer,
                                                                0,
                                                                totalRead,
                                                                bytesPerValue,
                                                                signedData);
            }
            return new DoubleData(doubleData, sampleRate, firstSample);
        }

        private void closeDataStream()
        {
            streamEndReached = true;
            if (dataStream != null) 
            {
                dataStream.Close();
            }
        }

        /// <summary>
        /// Returns the duration of the current data stream in milliseconds.
        /// </summary>
        /// <returns>the duration of the current data stream in milliseconds</returns>
        private long getDuration() 
        {
            return (long) (((double) totalValuesRead / (double) sampleRate)* 1000.0);
        }

    }
}
